package de.hpi.generator.generator.distributors;

import java.util.Random;

import de.hpi.generator.config.DistributionModel;
import de.hpi.generator.config.DistributionModelGAUSS;
import de.hpi.generator.generator.storages.ValueSeriaInfo;

/**
 * 
 * @author Michael
 * @see research.microsoft.com/pubs/68246/syntheticdatagen.pdf
 * @see http://en.wikipedia.org/wiki/Normal_distribution
 */
public class GaussianDistributor extends DataDistributor {

    protected static final int MAX_WRONG_VALUES = 10;
    /**
     * Expected Value (middle value with maximal count in range)
     */
    protected double aExpectedValue = 0;
    /**
     * Standard deviation (sigma) powered by 2 and multiplied on 2
     */
    protected double aSigmaQuad = 0;
    protected double aGaussCoefficient = 0;
    /**
     * a Values Count in the range
     */
    /**
     * start of range of values with nonzero counts
     */
    protected int aEffectiveRangeStart;
    /**
     * Length of range of values with nonzero counts
     */
    protected int aEffectiveRangeLength = 0;
    protected double aStdDeviation = 0;
 
    public String getName() {
        return "GAUSS";
    }

    public GaussianDistributor() {
        super();
        initClass(100, 1);
    }

    public GaussianDistributor(double aExpectedValue, double aStdDeviation) {
        super();
        initClass(aExpectedValue, aStdDeviation);
    }

    private void initClass(double aExpectedValue, double aStdDeviation) {
        this.aExpectedValue = aExpectedValue;
        this.aStdDeviation = aStdDeviation;
        this.aSigmaQuad = Math.pow(aStdDeviation, 2) * 2;
        this.aGaussCoefficient = 1 / Math.sqrt(Math.PI * this.aSigmaQuad);
    }

    @Override
    public void setupRange(int aValuesCount) throws Exception {
        super.setupRange(aValuesCount);
        calculateEffectiveRange();
        calculateAllValuesCount();
        this.random.calibrate(aEffectiveRangeStart, aEffectiveRangeLength);
    }

    private void calculateEffectiveRange() throws Exception {
        int aMaxDeviationOfExpectedValue = findMaxDeviationOfExpectedValue();
        // start of range of values with nonzero counts
        this.aEffectiveRangeStart = (int) (this.aExpectedValue - aMaxDeviationOfExpectedValue);
        // end of range of values with nonzero counts
        int aEffectiveRangeEnd = (int) (this.aExpectedValue + aMaxDeviationOfExpectedValue);
        this.aEffectiveRangeLength = Math.abs(aEffectiveRangeEnd - this.aEffectiveRangeStart) + 1;
    }

    private void calculateAllValuesCount() {
        aAllValuesCount = 0;
        for (int aValue = 0; aValue < this.aEffectiveRangeLength; aValue++) {
            aAllValuesCount += this.getCountOfValue(this.aEffectiveRangeStart + aValue);
        }
    }

    private int findMaxDeviationOfExpectedValue() throws Exception {
        int result = (int) this.aExpectedValue;
        checkForEnoughValuesCount(result);

        while (this.getCountOfValue(result) > 0) {
            result++;
        }
        return result - (int) this.aExpectedValue - 1;
    }

    private void checkForEnoughValuesCount(int result) throws Exception {
        if (this.getCountOfValue(result) == 0) {
            throw new Exception(
                    String.format(
                    "Cannot create distribution for this params (mu:%g sigma:%g values count:%d). Please use values count at least :%d.",
                    this.aExpectedValue,
                    this.aStdDeviation,
                    aNumberOfValuesToGenerate,
                    ((int)this.aStdDeviation) * 2));
        }
    }
    
    /**
     * setup object with specialized params
     * 
     * @param aSpecializedDistributionModel
     *            specialized params
     */
    @Override
    protected void setupObjectWithSpecializedParams(DistributionModel aSpecializedDistributionModel) throws Exception {
        super.setupObjectWithSpecializedParams(aSpecializedDistributionModel);
        // convert common type of aSpecializedDistributionModel object to
        // specialized type
        DistributionModelGAUSS aDistributionModelGAUSS = (DistributionModelGAUSS) aSpecializedDistributionModel;
        // setup distributor parameters
        this.initClass(aDistributionModelGAUSS.mu, aDistributionModelGAUSS.sigma);
    }

    /**
     * generates random positive integer
     * 
     * @return random positive integer
     */
    @Override
    public ValueSeriaInfo generateNextRandomValueSeriaInfo() throws Exception {
        int aRandomInteger = random.getRandomValue();
        int aRandomIntegerCount = this.getCountOfValue(aRandomInteger);

        return new ValueSeriaInfo(aRandomInteger, aRandomIntegerCount);
    }



    protected double gauss(double aValue) {
        double aPercent = aGaussCoefficient
                * Math.exp(-1
                * (Math.pow(aValue - this.aExpectedValue, 2) / aSigmaQuad));

        return aPercent;
    }

    /**
     * Returns how many times the value should be generated in a column
     * dependent on Gauss distribution
     * 
     * @param aValue
     * @return how many times the value should be generated in a column
     *         dependent on Gauss distribution
     */
    @Override
    public int getCountOfValue(int aValue) {
        return (int) Math.round(this.aNumberOfValuesToGenerate * gauss(aValue));
    }

}
